/*
 * ./core/de/mhu/lib/ACast.java
 *  Copyright (C) 2002-2004 Mike Hummel
 *
 *  This library is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published
 *  by the Free Software Foundation; either version 2.1 of the License, or
 *  (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package de.mhus.lib.core;

import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import de.mhus.lib.core.logging.Log;


/**
 * 
 * Smplifies casts between java classes. Some functions in this class only make
 * the code readable. e.g. from string to int.
 * <p>
 * All Funktions are static.
 * 
 * @author jesus
 */
public final class MCast {

	private static Log log = Log.getLog(MCast.class);
	private static SimpleDateFormat dateFormat = new SimpleDateFormat(
			"yyyy-MM-dd_HH:mm:ss.SSS z");
	private static DecimalFormat doubleFormat = new DecimalFormat("0.##########");

	private static final char[] HEX_CHAR_TABLE = {
	    '0', '1', '2', '3','4', '5', '6', '7',
	    '8', '9', 'a', 'b','c', 'd', 'e', 'f'
	};
	
	/**
	 * Will round the value mathematically and
	 * return every time a comma as separator and
	 * two digits after comma.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toCurrencyString(double _in) {

		// round
		_in = Math.round(_in * 100d) / 100d;
		// out
		StringBuffer out = new StringBuffer();
		out.append(_in);

		// change "." to ","
		int pos = out.indexOf( "." );
		if ( pos >= 0 )
			out.setCharAt(pos, ',');
		else
			pos = out.indexOf(",");
		
		if ( pos <= 0 )
			out.append(",00");
		else
		if ( out.length() - pos <= 2 )
			out.append("0");

		return out.toString();

	}

	/**
	 * Try to parse a String and return the equivalent Date object. The string
	 * should contain a iso date string like "yyyy-mm-dd" syntax:
	 * "yyyy-mm-dd[[ HH:MM:SS].XXX]" where XXX is Millisecond. Milliseconds are
	 * ignored. For the date part there are alternative syntax: "dd.mm.yyyy" or
	 * "mm/dd/yyyy".
	 * <p>
	 * If the time is not in the string, it will be set to "00:00:00". It is
	 * possible to leave year, in this case it will be replaced with the actuall
	 * year. If you leave month, it will be replaced with the actuall month.
	 * 
	 * @param _in
	 *            Iso DateTime
	 * @return In all cases an Date() object. Is getTime() is 0, it occurs an
	 *         error: ACast.toDate( in ).getTime == 0.
	 */

	public static Date toDate(String _in,Date _def) {
		Calendar c = toCalendar(_in);
		if (c == null)
			return _def;
		return c.getTime();
	}

	public static Calendar toCalendar(Date _in) {
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(_in);
		return calendar;
	}
	
	public static Calendar toCalendar(Date _in, TimeZone tz, Locale l) {
		Calendar calendar = Calendar.getInstance(tz,l);
		calendar.setTime(_in);
		return calendar;
	}
	
	public static Calendar toCalendar(String _in) {

		try {
			String date = _in.trim();

			int hour = 0;
			int min = 0;
			int sec = 0;
			int millies = 0;
			String zone = null;

			char sep = '?';
			if ( MString.isIndex(date, '_' ) )
				sep = '_';
			else
			if ( MString.isIndex(date, ' ' ) )
				sep = ' ';
			
			if (sep != '?') {

				// is time part on the end....
				String time = MString.afterIndex(date, sep).trim();
				date = MString.beforeIndex(_in, sep).trim();

				// zone
				char sep2 = '?';
				if ( MString.isIndex(time, ' ' ) )
					sep2 = ' ';
				else
				if ( MString.isIndex(time, '_' ) )
					sep2 = '_';
				if (sep2 != '?') {
					zone = MString.afterIndex(time, sep2);
					time = MString.beforeIndex(time, sep2);
				}

				// milliseconds
				if (MString.isIndex(time, '.')) {
					millies = toint(MString.afterIndex(time, '.'), 0);
					time = MString.beforeIndex(time, '.');
				}

				// parse time
				String[] parts = time.split("\\:");
				if (parts.length > 1) {
					hour = toint(parts[0], 0);
					min = toint(parts[1], 0);
					if (parts.length > 2)
						sec = toint(parts[2], 0);
				}

			}

			String[] parts = date.split("-");
			Calendar c = Calendar.getInstance();
			c.clear();
			
			if (zone != null) {
				TimeZone tz = TimeZone.getTimeZone(zone);
				c.setTimeZone(tz);
			}

			if (parts.length == 3) {
					int year = Integer.parseInt(parts[0]);
					if (parts[0].length()==2) year = year + 2000; // will this lib life for 100 years ???
					int month = Integer.parseInt(parts[1])-1;
					int day   = Integer.parseInt(parts[2]);
					c.set(year,month, day);
			} else if (parts.length == 2) {
				c.set(Calendar.MONTH, Integer.parseInt(parts[0]) - 1);
				c.set(Calendar.DATE, Integer.parseInt(parts[1]));
			} else {
				parts = date.split("\\.");
				if (parts.length == 3) {
					int year = Integer.parseInt(parts[2]);
					if (parts[2].length()==2) year = year + 2000; // will this lib life for 100 years ???
					int month = Integer.parseInt(parts[1])-1;
					int day   = Integer.parseInt(parts[0]);
					c.set(year,month,day);
				} else if (parts.length == 2) {
					int year = Integer.parseInt(parts[1]);
					if (parts[1].length()==2) year = year + 2000; // will this lib life for 100 years ???
					int month = Integer.parseInt(parts[0])-1;					
					c.set(Calendar.MONTH, month);
					c.set(Calendar.YEAR, year);
				} else {
					parts = date.split("/");
					if (parts.length == 3)
						c.set(Integer.parseInt(parts[2]), Integer
										.parseInt(parts[0])-1, Integer
										.parseInt(parts[1]));

					if (parts.length == 2) {
						c.set(Calendar.MONTH, Integer.parseInt(parts[0]) - 1);
						c.set(Calendar.YEAR, Integer.parseInt(parts[1]));
					}
				}
			}

//			if (zone != null) {
//				TimeZone tz = TimeZone.getTimeZone(zone);
//				c.setTimeZone(tz);
//			}
			
			c.set(Calendar.HOUR_OF_DAY, hour);
			c.set(Calendar.MINUTE, min);
			c.set(Calendar.MILLISECOND, sec * 1000 + millies);

			return c;

		} catch (Throwable e) {
			log.t(_in,e);
		}

		// return unknown - timestamp is 0
		return null;
	}

	/**
	 * Return the date as string with format: yyyy-MM-dd_HH:mm:ss.SSS
	 * using a date formater.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(Date _in) {
		if (_in == null) return null;
		// return _in.getDate() + "." + (_in.getMonth()+1) + "." +
		// (_in.getYear() + 1900 );
		synchronized (dateFormat) {
			return dateFormat.format(_in);
		}
	}

	/**
	 * Return the date as string with format: yyyy-MM-dd_HH:mm:ss.SSS
	 * using a date formater.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(Calendar _in) {
		if (_in == null) return null;
		synchronized (dateFormat) {
			return dateFormat.format(_in.getTime());
		}
	}
	
	/**
	 * Returns the date in iso format: yyyy-mm-dd
	 * 
	 * @param _in
	 * @return
	 */
	public static String toIsoDate(Date _in) {
		Calendar c = Calendar.getInstance();
		c.setTime(_in);
		return toIsoDate(c);
	}

	/**
	 * Returns the date in iso time format: yyyy-mm-dd HH:mm:ss.SSS
	 * 
	 * @param _in
	 * @return
	 */
	public static String toIsoDateTime(Date _in) {
		Calendar c = Calendar.getInstance();
		c.setTime(_in);
		return toIsoDateTime(c);
	}
	
	/**
	 * Convert the byte array to a string representation. It stores for every
	 * byte a two letter hex value in the string.
	 * 
	 * @param in
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	public static String toBinaryString(byte[] in ) {
		char[] hex = new char[2 * in.length];
	    int index = 0;

	    for (byte b : in) {
	      int v = b & 0xFF;
	      hex[index++] = HEX_CHAR_TABLE[v >>> 4];
	      hex[index++] = HEX_CHAR_TABLE[v & 0xF];
	    }
	    return new String(hex);
	}
	
	/**
	 * Convert a string with hex values in a byte array.
	 * 
	 * @see toBinaryString
	 * @param in
	 * @return
	 */
	public static byte[] fromBinaryString(String in) {
		byte[] out = new byte[ in.length() / 2 ];
		for ( int i = 0; i < out.length; i++ )
			out[i] = byteFromHex( in, i*2 );
		return out;
	}

	/**
	 * Convert a two letter hex value to a single byte value.
	 * 
	 * @param in
	 * @param offset
	 * @return
	 */
	public static byte byteFromHex( String in, int offset ) {
		int i = Integer.parseInt(in.substring(offset, offset+2), 16);
		byte b =(byte)( i & 0xFF );
        return b;
	}
	
	/**
	 * Convert a byte to a two letter hex value.
	 * 
	 * @param in
	 * @return
	 * @throws UnsupportedEncodingException
	 */
	public static String toHex2String( byte in ) {
		char[] hex = new char[2];

       int v = in & 0xFF;
       hex[0] = HEX_CHAR_TABLE[v >>> 4];
       hex[1] = HEX_CHAR_TABLE[v & 0xF];
       
	   return new String(hex);
	}
	
	public static String toIsoDateTime(Calendar _in) {
		return _in.get(Calendar.YEAR) + "-"
				+ toString(_in.get(Calendar.MONTH) + 1, 2) + "-"
				+ toString(_in.get(Calendar.DAY_OF_MONTH), 2) + " "
				+ toString(_in.get(Calendar.HOUR_OF_DAY), 2) + ":"
				+ toString(_in.get(Calendar.MINUTE), 2) + ":"
				+ toString(_in.get(Calendar.SECOND), 2) 
				// + "." + toString(_in.get(Calendar.MILLISECOND), 3)
				;
	}

	/**
	 * Calendar to iso date: yyyy-mm-dd
	 * 
	 * @param _in
	 * @return
	 */
	public static String toIsoDateTime(long timeStamp) {
		
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis(timeStamp);
		return toIsoDateTime(c);
		
	}
	
	/**
	 * Calendar to iso date: yyyy-mm-dd
	 * 
	 * @param _in
	 * @return
	 */
	public static String toIsoDate(Calendar _in) {
		return _in.get(Calendar.YEAR) + "-"
				+ toString(_in.get(Calendar.MONTH) + 1, 2) + "-"
				+ toString(_in.get(Calendar.DAY_OF_MONTH), 2);
	}

	/**
	 * Convert String to boolean. If the conversion was not possible it returns
	 * "_default".
	 * 
	 * Valide true values: yes, true, ja, 1, t
	 * 
	 * valide false values: no, falsae, nein, 0, f
	 * @param _in
	 * @param _default
	 * @return
	 */
	public static boolean toboolean(Object _in, boolean _default) {

		if (_in == null)
			return _default;

		if (_in instanceof Boolean)
			return (Boolean)_in;
		
		if (_in instanceof Number)
			return !(((Number)_in).intValue() == 0);
		
		String in = _in.toString().toLowerCase().trim();

		if (in.equals("yes"))
			return true;
		if (in.equals("on"))
			return true;
		if (in.equals("true"))
			return true;
		if (in.equals("ja"))  // :-)
			return true;
		if (in.equals("tak")) // :-)
			return true;
		if (in.equals("oui")) // :-)
			return true;
		if (in.equals("si")) // :-)
			return true;
		if (in.equals("\u4fc2")) // :-) chinese
			return true;
		if (in.equals("HIja'")) // :-) // klingon
			return true;
		if (in.equals("1"))
			return true;
		if (in.equals("t"))
			return true;
		if (in.equals("y"))
			return true;
		if (in.equals("\u2612"))
			return true;

		if (in.equals("no"))
			return false;
		if (in.equals("off"))
			return false;
		if (in.equals("false"))
			return false;
		if (in.equals("nein")) // :-)
			return false;
		if (in.equals("nie"))  // :-)
			return false;
		if (in.equals("non"))  // :-)
			return false;
		if (in.equals("\u5514\u4fc2"))  // :-) chinese
			return false;
		if (in.equals("Qo'"))  // :-) klingon
			return false;
		if (in.equals("0"))
			return false;
		if (in.equals("-1"))
			return false;
		if (in.equals("f"))
			return false;
		if (in.equals("n"))
			return false;
		if (in.equals("\u2610"))
			return false;

		return _default;

	}

	/**
	 * Convert a string to float. If the string is malformed it returns
	 * "_def".
	 * 
	 * @param _in
	 * @param _def
	 * @return
	 */
	public static float tofloat(Object _in, float _def) {
		
		if (_in == null) return _def;
		if (_in instanceof Number)
			return ((Number)_in).floatValue();
		
		try {
			return Float.parseFloat(String.valueOf(_in));
		} catch (Throwable e) {
			log.t(_in,e.toString());
		}
		return _def;
	}

	/**
	 * Convert a string to double. If the string is malformed it returns
	 * "_def".
	 */
	public static double todouble(Object _in, double _def) {
		
		if (_in == null) return _def;
		if (_in instanceof Number)
			return ((Number)_in).doubleValue();
		
		try {
			return Double.parseDouble(String.valueOf(_in));
		} catch (Throwable e) {
			log.t(_in,e.toString());
		}
		return _def;
	}

	/**
	 * Converts String to int. If the string is malformed then it returns "_def". 
	 * A valid format is also the hex 0x (e.g. 0xFFFF) variant.
	 * 
	 * @param _in
	 * @param _def
	 * @return
	 */
	public static int toint(Object in, int _def) {

		if (in == null) return _def;
		if (in instanceof Number)
			return ((Number)in).intValue();

		String _in = String.valueOf(in);
		try {
			if (_in.startsWith("0x")) {
				int out = 0;
				for (int i = 2; i < _in.length(); i++) {
					int s = -1;
					char c = _in.charAt(i);
					if (c >= '0' && c <= '9')
						s = c - '0';
					else if (c >= 'a' && c <= 'f')
						s = c - 'a' + 10;
					else if (c >= 'A' && c <= 'F')
						s = c - 'A' + 10;

					if (s == -1)
						throw new NumberFormatException(_in);
					out = out * 16 + s;
				}
				return out;
			}
			
			return Integer.parseInt(_in);
		} catch (Throwable e) {
			log.t(_in, e.toString());
			return _def;
		}

	}

	/**
	 * Converts a string to long. If the string is malformed then it returns "_def". 
	 * A valid format is also the hex 0x (e.g. 0xFFFF) variant.
	 * 
	 * @param _in
	 * @param _def
	 * @return
	 */
	public static long tolong(Object in, long _def) {
		
		if (in == null) return _def;
		if (in instanceof Number)
			return ((Number)in).longValue();

		String _in = String.valueOf(in);
		
		try {
			
			if (_in.startsWith("0x")) {
				long out = 0;
				for (int i = 2; i < _in.length(); i++) {
					int s = -1;
					char c = _in.charAt(i);
					if (c >= '0' && c <= '9')
						s = c - '0';
					else if (c >= 'a' && c <= 'f')
						s = c - 'a' + 10;
					else if (c >= 'A' && c <= 'F')
						s = c - 'A' + 10;

					if (s == -1)
						throw new NumberFormatException(_in);
					out = out * 16 + s;
				}
				return out;
			}
			
			return Long.parseLong(_in);
		} catch (Throwable e) {
			log.t(_in, e.toString());
		}
		return _def;
	}

	/**
	 * Convert a double to string. The separator is
	 * a dot.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(double _in) {
		String out = doubleFormat.format(_in);
		if (out.indexOf(',') >= 0) out = out.replace(",", "."); // for secure
		return out;
	}

	/**
	 * Convert a double to string. The separator is
	 * a dot.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(float _in) {
		return Float.toString(_in).replace(",", ".");
	}
	
	/**
	 * Convert a boolean to string. Values are "true", "false".
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(boolean _in) {
		if (_in)
			return "true";
		else
			return "false";
	}

	/**
	 * Converts integer to String.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(int _in) {
		return Integer.toString(_in);
	}

	/**
	 * Converts integer to string with the minimum digits.
	 * 
	 * @param _in
	 * @param _numbers
	 * @return
	 */
	public static String toString(int _in, int _digits) {
		// FIXME performance please !
		String out = Integer.toString(_in);
		while (out.length() < _digits)
			out = "0" + out;
		return out;
	}

	/**
	 * Convert long to string.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toString(long _in) {
		return String.valueOf(_in);
	}

	
	/**
	 * Convert integer to two letter hex code. Ignores negative values.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toHex2String(int _in) {
		String out = Integer.toHexString(_in).toUpperCase();
		if (out.length() == 1)
			out = "0" + out;
		return out;
	}

	/**
	 * Convert integer to four letter hex code. Ignores negative values.
	 * 
	 * @param _in
	 * @return
	 */
	public static String toHex4String(int _in) {
		return toHex2String(_in / 256) + toHex2String(_in % 256);
	}

	/**
	 * Put all list elements in a string list. Use the toString method.
	 * 
	 * @param _v
	 * @return
	 */
	public static String[] toStringArray(List<?> _v) {

		String[] out = new String[_v.size()];
		for (int i = 0; i < _v.size(); i++) {
			Object o = _v.get(i);
			if (o == null)
				out[i] = null;
			else
				out[i] = o.toString();
		}
		return out;
	}


	public static int tointFromHex(String _in) {

		int out = 0;
		for (int i = 0; i < _in.length(); i++) {
			int x = 0;
			char c = _in.charAt(i);
			if (c >= '0' && c <= '9')
				x = (c - '0');
			else if (c >= 'a' && c <= 'f')
				x = (c - 'a' + 10);
			else if (c >= 'A' && c <= 'F')
				x = (c - 'A' + 10);
			else
				throw new NumberFormatException(_in);
			out = out * 16 + x;
		}

		return out;
	}

	public static String toString(byte[] in) {

		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < in.length; i++) {
			if (i != 0)
				sb.append(',');
			sb.append(Byte.toString(in[i]));
		}
		return sb.toString();

	}

	public static byte[] toByteArray(String in) {

		if (in.length() == 0)
			return new byte[0];

		int offset = 0;
		int cnt = 0;

		while ((offset = in.indexOf(',', offset + 1)) >= 0) {
			cnt++;
		}

		offset = 0;
		int old = 0;
		byte[] out = new byte[cnt + 1];
		cnt = 0;
		while ((offset = in.indexOf(',', offset + 1)) >= 0) {
			out[cnt] = Byte.parseByte(in.substring(old, offset));
			cnt++;
			old = offset + 1;
		}

		out[cnt] = Byte.parseByte(in.substring(old));

		return out;
	}

	public static String toString(String firstLine, StackTraceElement[] trace) {
		StringBuffer sb = new StringBuffer();
		if (firstLine != null)
			sb.append(firstLine).append('\n');
		if (trace == null)
			return sb.toString();

		for (int i = 0; i < trace.length; i++)
			sb.append("\tat ").append(trace[i].getClassName()).append('.')
					.append(trace[i].getMethodName()).append('(').append(
							trace[i].getFileName()).append(':').append(
							trace[i].getLineNumber()).append(")\n");
		return sb.toString();
	}
	
	/**
	 * Return an indexed map of the values. The first value has the index "0" and so on.
	 * 
	 * @param values
	 * @return
	 */
	public static Map<String,Object> toIndexedMap(Object ... values) {
		HashMap<String,Object> out = new HashMap<String, Object>();
		for (int i = 0; i < values.length; i++) {
			out.put(toString(i), values[i]);
		}
		return out;
	}

	public static String objectToString(Object value) {
		
		if (value == null) return null;
		
		if (value instanceof Integer)
			return toString((Integer)value);
		if (value instanceof Long)
			return toString((Long)value);
		if (value instanceof Double)
			return toString((Double)value);
		if (value instanceof Float)
			return toString((Float)value);
		if (value instanceof Date)
			return toString((Date)value);
		if (value instanceof Calendar)
			return toString((Calendar)value);

		return value.toString();
	}

	public static Date objectToDate(Object value) {
		if (value == null) return null;
		if (value instanceof Date)
			return (Date)value;
		if (value instanceof Calendar)
			return ((Calendar)value).getTime();
		return toDate(String.valueOf(value), null);
	}

	public static java.sql.Date toSqlDate(java.sql.Date date) {
		return new java.sql.Date(date.getTime());
	}

}
